""" Defines the DynamicGroup and related functions.
    
    This module defines the DynamicGroup class, a subclass of
    pygame.sprite.AbstractGroup. Unlike AbstractGroup, each instance
    of this type of group has a string ID (you might call it a GID
    string) and holds only weak references to its sprites.
    
    This module also defines a very simple system for dynamically
    creating DynamicGroups as needed and "tracking" them. It can
    be used to decide automatically whether or not a group with the
    given GID exists and either get a reference to it (if it does
    exist) or create it (if it does not exist). See the get() function
    for more details about this.
    
    NOTE: Unlike many other modules in sbak, this module does NOT need
    to be initialized before use. (There is no init() function.)
    
    FIXME: This module has a confusing name...
"""

import weakref
from pygame import sprite

__all__ = (
    'DynamicGroup',
    'DynamicSprite'
    'exists',
    'get',
    'contains',
)

_group = weakref.WeakValueDictionary() # All DynamicGroups.


class DynamicGroup(sprite.AbstractGroup):
    
    def __init__(self,ID,*sprites):
        """ Initializes a DynamicGroup object.
        
            ID:       Give a string ID for the group.
            *sprites: Give a sequence of sprites to add to the
                      group initially.
        """
        self.id = ID
        sprite.AbstractGroup.__init__(self)
        self.spritedict = weakref.WeakKeyDictionary()
        self.add(*sprites)
    
    def __repr__(self):
        return '<%s "%s" (%i sprites)>'%(
            self.__class__.__name__, self.id, len(self.sprites()) )
        
    def __str__(self):
        return self.id
    
    def get_seq_str(self):
        """ Returns the id of the group.
        
            The presence of this method in objects indicates that the
            object is compatible with dynamic ID-string replacement in
            sequences.
        """
        return self.id 

class DynamicSprite(sprite.Sprite):
    """ Sprite that is aware of the DynamicGroup system.
    
        Any place where a group object is expected a group ID may be
        used. You can even mix group IDs and actual group objects.
        When an ID for a non-existing DynamicGroup is given the
        group will be created using sbak.group.get().
    """
    
    def __init__(self,*groups):
        """ Initializes a DynamicSprite object.
            
            *groups:    One or more groups or sequence of groups. Each
                        item may be a group object or a DynamicGroup
                        ID string.
        """
        self._group_ids = {}
        super(DynamicSprite,self).__init__(*groups)
    
    def add(self, *groups):
        """ Adds the entity to one or more groups.
        
            Groups may be strings. If a group is a string, the entity will be
            added to the DynamicGroup with that string as its ID. If there is
            no DynamicGroup with that ID, it will be created.
        """
        has = self._Sprite__g.has_key
        for group in groups:
            if isinstance(group, basestring):
                group = get(group)
            if hasattr(group, '_spritegroup'):
                if not has(group):
                    group.add_internal(self)
                    self.add_internal(group)
            else: self.add(*group)
    
    def remove(self, *groups):
        """ Removes the entity from one or more groups.
        
            Groups may be strings. If a group is a string, the entity will be
            removed from the DynamicGroup with that string as its ID. If the
            entity is not in a group with the given ID, a KeyError will be
            raised.
        """
        has = self._Sprite__g.has_key
        for group in groups:
            if isinstance(group, basestring):
                group = self._group_ids[group]
            if hasattr(group, '_spritegroup'):
                if has(group):
                    group.remove_internal(self)
                    self.remove_internal(group)
            else: self.remove(*group)
    
    def add_internal(self, g):
        self._Sprite__g[g] = 0
        self._group_ids[g.id if hasattr(g,'id') else id(g)] = g

    def remove_internal(self, g):
        del self._Sprite__g[g]
        del self._group_ids[g.id if hasattr(g,'id') else id(g)]
    
    def group_ids(self):
        """ Returns a list of the IDs of all groups that the entity is in. """
        return self._group_ids.keys()


def get(ID,*sprites):
    """ Either return an existing DynamicGroup or create a new one.
    
        If a DynamicGroup with the given ID has already been created
        with this function it will return that existing group. If no
        group with the given ID has been created with this function
        the group will be created and returned. Either way, the given
        sprites will be added to the group.
        
        Every DynamicGroup created with this function is stored in a
        WeakValueDictionary. This means that every group created will
        be removed from the dictionary automatically when all other
        references to the group are gone. (This includes references
        to the group held by the sprites it contains.)
    """
    if ID in _group:
        _group[ID].add(*sprites)
        return _group[ID]
    else:
        g = _group[ID] = DynamicGroup(ID, *sprites)
        return g

def exists(ID):
    " Returns whether or not a DynamicGroup with the given ID string exists. "
    return ID in _group

def contains(seq, g):
    """ Returns whether or not the object `g` is in the sequence `seq`.
    
        Checks for the method get_seq_str in each item of seq. If
        present, the object is substituded for the return value of the
        method. (The same is true for g.)
        
        If `g` is present in the sequence `seq` according to the rules
        above, contains(seq,g) returns True. If not, False is returned.
        
        This function is intended to make it easier to determine
        whether or not a DynamicGroup is in a sequence when only the ID
        of the DynamicGroup is known. However, any type of object may
        be used. In that respect it works the same as the `in` keyword,
        except for the substitutions explained above.
    """
    seq = [i if not hasattr(i,'get_seq_str') else i.get_seq_str() for i in seq]
    g = g if not hasattr(g,'get_seq_str') else g.get_seq_str()
    yes = g in seq
    return yes
        